﻿using System;
using System.Text;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Configuration;
using System.Configuration.Provider;
using System.Security.Cryptography;
using System.Web.Security;
using System.Web.Configuration;
using System.Reflection;

namespace Ruandao.Account
{
    using Ruandao.Account.EntityAccess;
    using System.Transactions;
    using Ruandao.HostServer;

    public sealed class RuandaoMembershipProvider : MembershipProvider
    {
        //
        // Global connection string, generated password length, generic exception message, event log info.
        //
        private int newPasswordLength = 8;

        //
        // Used when determining encryption key values.
        //
        private MachineKeySection machineKey;

        //
        // System.Configuration.Provider.ProviderBase.Initialize Method
        //
        public override void Initialize(string name, NameValueCollection config)
        {
            //
            // Initialize values from web.config.
            //

            if (config == null)
                throw new ArgumentNullException("config");

            if (name == null || name.Length == 0)
                name = "RuandaoMembershipProvider";

            if (String.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", "Ruandao Membership provider");
            }

            // Initialize the abstract base class.
            base.Initialize(name, config);

            // Get encryption and decryption key information from the configuration.
            Configuration cfg =
              WebConfigurationManager.OpenWebConfiguration(System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
            machineKey = (MachineKeySection)cfg.GetSection("system.web/machineKey");

            // 下述代码会抛出异常：原因是PasswordFormat需要HttpContext,而在加载web.config的时候HttpContext为空。
            //if (machineKey.ValidationKey.Contains("AutoGenerate"))
            //    if (PasswordFormat != MembershipPasswordFormat.Clear)
            //        throw new ProviderException("Hashed or Encrypted passwords " +
            //                                    "are not supported with auto-generated keys.");

            //ApplicationName = "AccountContext(" + accountContextCode + ")";
        }


        //
        // A helper function to retrieve config values from the configuration file.
        //
        private string GetConfigValue(string configValue, string defaultValue)
        {
            if (String.IsNullOrEmpty(configValue))
                return defaultValue;

            return configValue;
        }

        // 需要增加缓存来提升性能。。。
        private AccountContextEntity GetAccountContext()
        {
            return AccountContextEntity.FetchByName(SiteWare.Current.AccountContextName);
        }

        //
        // System.Web.Security.MembershipProvider properties.
        //

        public override string ApplicationName 
        {
            get { throw new NotSupportedException(); }
            set { throw new NotSupportedException(); }
        }

        public override bool EnablePasswordReset
        {
            get { return GetAccountContext().EnablePasswordReset; }
        }

        public override bool EnablePasswordRetrieval
        {
            get { return GetAccountContext().EnablePasswordRetrieval; }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get { return GetAccountContext().RequiresQuestionAndAnswer; }
        }

        public override bool RequiresUniqueEmail
        {
            get { return GetAccountContext().RequiresUniqueEmail; }
        }


        public override int MaxInvalidPasswordAttempts
        {
            get { return GetAccountContext().MaxInvalidPasswordAttempts; }
        }

        public override int PasswordAttemptWindow
        {
            get { return GetAccountContext().PasswordAttemptWindow; }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get 
            {
                if (machineKey.ValidationKey.Contains("AutoGenerate"))
                    if (PasswordFormat != MembershipPasswordFormat.Clear)
                        throw new ProviderException("Hashed or Encrypted passwords " +
                                                    "are not supported with auto-generated keys.");

                return (MembershipPasswordFormat)Enum.Parse(typeof(MembershipPasswordFormat), GetAccountContext().PasswordFormat); 
            }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get { return GetAccountContext().MinRequiredPasswordSymbols; }
        }

        public override int MinRequiredPasswordLength
        {
            get { return GetAccountContext().MinRequiredPasswordLength; }
        }

        public override string PasswordStrengthRegularExpression
        {
            get { return GetAccountContext().PasswordStrengthRegularExpression; }
        }

        //
        // System.Web.Security.MembershipProvider methods.
        //

        //
        // MembershipProvider.ChangePassword
        //
        public override bool ChangePassword(string username, string oldPwd, string newPwd)
        {
            if (!ValidateUser(username, oldPwd))
                return false;


            ValidatePasswordEventArgs args =
              new ValidatePasswordEventArgs(username, newPwd, true);

            OnValidatingPassword(args);

            if (args.Cancel)
            {
                if (args.FailureInformation != null)
                    throw args.FailureInformation;
                else
                    throw new MembershipPasswordException("Change password canceled due to new password validation failure.");
            }

            RegisterUserEntity entity = RegisterUserEntity.FetchByLoweredName(username.ToLower());
            entity.Password = EncodePassword(newPwd);
            entity.LastPasswordChangedDate = DateTime.Now;
            if( entity.Save() == 1 )
            {
                return true;
            }
            return false;
        }

        //
        // MembershipProvider.ChangePasswordQuestionAndAnswer
        //
        public override bool ChangePasswordQuestionAndAnswer(string username,
                      string password,
                      string newPwdQuestion,
                      string newPwdAnswer)
        {
            if (!ValidateUser(username, password))
                return false;

            //RegisterUserEntity entity = RegisterUserEntity.FetchByUIX_AccountContextCode_LoweredUserName(this.GetAccountContext().AccountContextCode, username.ToLower());
            RegisterUserEntity entity = RegisterUserEntity.FetchByLoweredName( username.ToLower());
            entity.PasswordQuestion = newPwdQuestion;
            entity.PasswordAnswer = EncodePassword(newPwdAnswer);
            if( entity.Save() == 1 )
            {
                return true;
            }
            return false;
        }

        //
        // MembershipProvider.CreateUser
        //
        public override MembershipUser CreateUser(string username,
                 string password,
                 string email,
                 string passwordQuestion,
                 string passwordAnswer,
                 bool isApproved,
                 object providerUserKey,
                 out MembershipCreateStatus status)
        {
            ValidatePasswordEventArgs args =
              new ValidatePasswordEventArgs(username, password, true);

            OnValidatingPassword(args);

            if (args.Cancel)
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }

            if (RequiresUniqueEmail &&
                RegisterUserEntity.FetchByLoweredEmail(email.ToLower()) != null) 
            {
                status = MembershipCreateStatus.DuplicateEmail;
                return null;
            }

            if (RegisterUserEntity.FetchByLoweredName(username.ToLower()) != null)
            {
                status = MembershipCreateStatus.DuplicateUserName;
                return null;
            }

            DateTime createDate = DateTime.Now;

            if (providerUserKey == null)
            {
                providerUserKey = Guid.NewGuid();
            }
            else
            {
                if (!(providerUserKey is Guid))
                {
                    status = MembershipCreateStatus.InvalidProviderUserKey;
                    return null;
                }
            }

            try{
                CreateRegisterUser(
                    username,
                    password,
                    email,
                    passwordQuestion,
                    passwordAnswer,
                    isApproved,
                    (Guid)providerUserKey);
            }
            catch(SecurityException except )
            {
                status = MembershipCreateStatus.UserRejected;
                return null;
            }
            status = MembershipCreateStatus.Success;
            return GetUser(username, false);
        }

        private void CreateRegisterUser(
                string username,
                 string encodePassword,
                 string email,
                 string passwordQuestion,
                 string encodePasswordAnswer,
                 bool isApproved,
                 Guid userGuid)
        {
            UserEntity ue = UserEntity.Fetch(userGuid);
            if (ue == null)
            {
                ue = new UserEntity();
                ue.Guid = userGuid;
            }
            ue.IsAnonymous = false;
            ue.LastActivityDate = DateTime.Now;

            RegisterUserEntity rue = new RegisterUserEntity();
            rue.AccountContextId = GetAccountContext().Id;
            rue.Guid = userGuid;
            rue.Name = username;
            rue.LoweredName = username.ToLower();
            rue.Password = encodePassword;
            rue.Email = email;
            rue.LoweredEmail = email.ToLower();
            rue.PasswordQuestion = passwordQuestion;
            rue.PasswordAnswer = encodePasswordAnswer;
            rue.IsApproved = isApproved;
            rue.Comment = "";
            rue.CreateDate = DateTime.Now;
            rue.LastPasswordChangedDate = DateTime.Now;
            rue.IsLockedOut = false;
            rue.LastLockedOutDate = DateTime.Now;
            rue.FailedPasswordAttemptCount = 0;
            rue.FailedPasswordAttemptWindowStart = DateTime.Now;
            rue.FailedPasswordAnswerAttemptCount = 0;
            rue.FailedPasswordAnswerAttemptWindowStart = DateTime.Now;

            rue.LastLoginDate = DateTime.Now;
            rue.PasswordSalt = "s";

            try
            {
                using (TransactionScope ts = new TransactionScope())
                {
                    ue.Save();
                    rue.Save();
                    ts.Complete();
                }
            }
            catch (TransactionException e)
            {
                throw new SecurityException("创建用户" + userGuid.ToString() + "失败", e);
            }

        }
        //
        // MembershipProvider.DeleteUser
        //
        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            int result = RegisterUserEntity.DeleteByLoweredName (username.ToLower());
            return result == 0 ? false : true;
        }

        //
        // MembershipProvider.GetAllUsers
        //
        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            IList<RegisterUserEntity> all = RegisterUserEntity.GetAllEntities();
            IList<RegisterUserEntity> list = PageFromEntities(all, pageIndex, pageSize, out totalRecords);
            MembershipUserCollection users = new MembershipUserCollection();
            foreach ( RegisterUserEntity u in list)
            {
                users.Add(GetMembershipUser(u));
            }
            return users;
        }

        //
        // MembershipProvider.GetNumberOfUsersOnline
        //
        public override int GetNumberOfUsersOnline()
        {
            TimeSpan onlineSpan = new TimeSpan(0, System.Web.Security.Membership.UserIsOnlineTimeWindow, 0);
            DateTime compareTime = DateTime.Now.Subtract(onlineSpan);
            return AccountAccess.GetInstance()
                .SearchEntities<UserEntity>("WHERE LastActivityDate >= '" + compareTime.ToUniversalTime() + "'")
                .Count;
        }

        //
        // MembershipProvider.GetPassword
        //
        public override string GetPassword(string username, string answer)
        {
            if (!GetAccountContext().EnablePasswordRetrieval)
            {
                throw new ProviderException("Password Retrieval Not Enabled.");
            }

            if (GetAccountContext().PasswordFormat == "Hashed")
            {
                throw new ProviderException("Cannot retrieve Hashed passwords.");
            }
            RegisterUserEntity user = GetUserByName(username);

            if (user == null)
                throw new MembershipPasswordException("无效的用户名：" + username);
            if (user.IsLockedOut)
                throw new MembershipPasswordException("The supplied user is locked out.");

            string password = "";

            if (GetAccountContext().RequiresQuestionAndAnswer && !CheckPassword(answer, user.PasswordAnswer))
            {
                UpdatePasswordAnswerFailureCount(username);

                throw new MembershipPasswordException("Incorrect password answer.");
            }

            if (GetAccountContext().PasswordFormat == "Encrypted")
            {
                password = UnEncodePassword(user.Password);
            }

            return password;
        }

        //
        // MembershipProvider.GetUser(string, bool)
        //
        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            RegisterUserEntity user = GetUserByName(username);
            if (user != null )
            {
                if (userIsOnline)
                {
                    UserEntity ue = UserEntity.Fetch(user.Guid);
                    ue.LastActivityDate = DateTime.Now;
                    ue.Save();
                }
                return GetMembershipUser(user);
            }
            return null;
        }

        //
        // MembershipProvider.GetUser(object, bool)
        //
        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            RegisterUserEntity user = RegisterUserEntity.Fetch((Guid)providerUserKey);
            if (user != null)
            {
                if (userIsOnline)
                {
                    UserEntity ue = UserEntity.Fetch(user.Guid);
                    ue.LastActivityDate = DateTime.Now;
                    ue.Save();
                }
                return GetMembershipUser(user);
            }
            return null;
        }

        //
        // GetUserFromReader
        //    A helper function that takes the current row from the OdbcDataReader
        // and hydrates a MembershiUser from the values. Called by the 
        // MembershipUser.GetUser implementation.
        //
        private MembershipUser GetMembershipUser(RegisterUserEntity rue)
        {
            return new MembershipUser(
                    this.Name,
                    rue.Name,
                    rue.Guid,
                    rue.Email,
                    rue.PasswordQuestion,
                    rue.Comment,
                    rue.IsApproved,
                    rue.IsLockedOut,
                    rue.CreateDate,
                    rue.LastLoginDate,
                    UserEntity.Fetch(rue.Guid).LastActivityDate,
                    rue.LastPasswordChangedDate,
                    rue.LastLockedOutDate);
        }

        //
        // MembershipProvider.UnlockUser
        //
        public override bool UnlockUser(string username)
        {
            RegisterUserEntity user = GetUserByName(username);
            if (user == null)
            {
                throw new ProviderException("无效的用户名称：" + username);
            }
            user.LastLockedOutDate = DateTime.Now;
            int result = user.Save();
            return result == 1 ? true : false;
        }

        //
        // MembershipProvider.GetUserNameByEmail
        //
        public override string GetUserNameByEmail(string email)
        {
            RegisterUserEntity user = GetUserByEmail(email);
            if (user == null) return null;
                
            return user.Name;
        }

        //
        // MembershipProvider.ResetPassword
        //
        public override string ResetPassword(string username, string answer)
        {
            if (!GetAccountContext().EnablePasswordReset)
            {
                throw new NotSupportedException("Password reset is not enabled.");
            }

            if (answer == null && GetAccountContext().RequiresQuestionAndAnswer)
            {
                UpdatePasswordAnswerFailureCount(username);

                throw new ProviderException("Password answer required for password reset.");
            }

            string newPassword =
              System.Web.Security.Membership.GeneratePassword(newPasswordLength, MinRequiredNonAlphanumericCharacters);


            ValidatePasswordEventArgs args =
              new ValidatePasswordEventArgs(username, newPassword, true);

            OnValidatingPassword(args);

            if (args.Cancel)
                if (args.FailureInformation != null)
                    throw args.FailureInformation;
                else
                    throw new MembershipPasswordException("Reset password canceled due to password validation failure.");

            RegisterUserEntity user = GetUserByName(username);

            if( user==null)
                throw new MembershipPasswordException("The supplied user name is not found.");

            if (user.IsLockedOut)
                throw new MembershipPasswordException("The supplied user is locked out.");

            if (GetAccountContext().RequiresQuestionAndAnswer && !CheckPassword(answer, user.PasswordAnswer))
            {
                UpdatePasswordAnswerFailureCount(username);
                throw new MembershipPasswordException("Incorrect password answer.");
            }

            user.Password = EncodePassword(newPassword);
            user.LastPasswordChangedDate = DateTime.Now;
            if (user.Save() == 0)
            {
                throw new MembershipPasswordException("User not found, or user is locked out. Password not Reset.");
            }
            else
            {
                return newPassword;
            }
        }

        //
        // MembershipProvider.UpdateUser
        //
        public override void UpdateUser(MembershipUser user)
        {
            RegisterUserEntity entity = GetUserByName(user.UserName);
            entity.Email = user.Email;
            entity.Comment = user.Comment;
            entity.IsApproved = user.IsApproved;
            entity.Save();
        }

        //
        // MembershipProvider.ValidateUser
        //
        public override bool ValidateUser(string username, string password)
        {
            RegisterUserEntity user = GetUserByName(username);
            if (user == null)
                return false;
            if (user.IsLockedOut || !user.IsApproved)
                return false;
            if (CheckPassword(password, user.Password))
            {
                user.LastLoginDate = DateTime.Now;
                user.Save();
                return true;
            }
            else
            {
                UpdatePasswordFailureCount(username);
            }
            return false;
        }

        private void UpdatePasswordFailureCount(string username)
        {
            RegisterUserEntity user = GetUserByName(username);

            DateTime windowEnd = user.FailedPasswordAttemptWindowStart.AddMinutes(PasswordAttemptWindow);

            if (user.FailedPasswordAttemptCount == 0 || DateTime.Now > windowEnd)
            {
                // First password failure or outside of PasswordAttemptWindow. 
                // Start a new password failure count from 1 and a new window starting now.
                user.FailedPasswordAttemptCount = 1;
                user.FailedPasswordAttemptWindowStart = DateTime.Now;

                if ( user.Save() == 0 )
                    throw new ProviderException("Unable to update password failure count and window start.");
            }
            else
            {
                if (user.FailedPasswordAttemptCount++ >= GetAccountContext().MaxInvalidPasswordAttempts)
                {
                    // Password attempts have exceeded the failure threshold. Lock out
                    // the user.
                    user.IsLockedOut = true;
                    user.LastLockedOutDate = DateTime.Now;

                    if (user.Save() == 0)
                        throw new ProviderException("Unable to lock out user.");
                }
                else
                {
                    // Password attempts have not exceeded the failure threshold. Update
                    // the failure counts. Leave the window the same.
                    if (user.Save() == 0)
                        throw new ProviderException("Unable to update failure count.");
                }
            }
        }

        private void UpdatePasswordAnswerFailureCount(string username)
        {
            RegisterUserEntity user = GetUserByName(username);

            DateTime windowEnd = user.FailedPasswordAnswerAttemptWindowStart.AddMinutes(GetAccountContext().PasswordAttemptWindow);

            if (user.FailedPasswordAnswerAttemptCount == 0 || DateTime.Now > windowEnd)
            {
                // First password failure or outside of PasswordAttemptWindow. 
                // Start a new password failure count from 1 and a new window starting now.
                user.FailedPasswordAnswerAttemptCount = 1;
                user.FailedPasswordAnswerAttemptWindowStart = DateTime.Now;

                if (user.Save() == 0)
                    throw new ProviderException("Unable to update password failure count and window start.");
            }
            else
            {
                if (user.FailedPasswordAnswerAttemptCount++ >= GetAccountContext().MaxInvalidPasswordAttempts)
                {
                    // Password attempts have exceeded the failure threshold. Lock out
                    // the user.
                    user.IsLockedOut = true;
                    user.LastLockedOutDate = DateTime.Now;

                    if (user.Save() == 0)
                        throw new ProviderException("Unable to lock out user.");
                }
                else
                {
                    // Password attempts have not exceeded the failure threshold. Update
                    // the failure counts. Leave the window the same.
                    if (user.Save() == 0)
                        throw new ProviderException("Unable to update failure count.");
                }
            }
        }

        //
        // CheckPassword
        //   Compares password values based on the MembershipPasswordFormat.
        //
        private bool CheckPassword(string password, string dbpassword)
        {
            string pass1 = password;
            string pass2 = dbpassword;

            switch (GetAccountContext().PasswordFormat)
            {
                case "Encrypted":
                    pass2 = UnEncodePassword(dbpassword);
                    break;
                case "Hashed":
                    pass1 = EncodePassword(password);
                    break;
                default:
                    break;
            }

            if (pass1 == pass2)
            {
                return true;
            }

            return false;
        }

        //
        // EncodePassword
        //   Encrypts, Hashes, or leaves the password clear based on the PasswordFormat.
        //
        private string EncodePassword(string password)
        {
            string encodedPassword = password;

            switch (GetAccountContext().PasswordFormat)
            {
                case "Clear":
                    break;
                case "Encrypted":
                    encodedPassword =
                      Convert.ToBase64String(EncryptPassword(Encoding.Unicode.GetBytes(password)));
                    break;
                case "Hashed":
                    HMACSHA1 hash = new HMACSHA1();
                    hash.Key = HexToByte(machineKey.ValidationKey);
                    encodedPassword =
                      Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password)));
                    break;
                default:
                    throw new ProviderException("Unsupported password format.");
            }

            return encodedPassword;
        }

        //
        // UnEncodePassword
        //   Decrypts or leaves the password clear based on the PasswordFormat.
        //
        private string UnEncodePassword(string encodedPassword)
        {
            string password = encodedPassword;

            switch (GetAccountContext().PasswordFormat)
            {
                case "Clear":
                    break;
                case "Encrypted":
                    password =
                      Encoding.Unicode.GetString(DecryptPassword(Convert.FromBase64String(password)));
                    break;
                case "Hashed":
                    throw new ProviderException("Cannot unencode a hashed password.");
                default:
                    throw new ProviderException("Unsupported password format.");
            }

            return password;
        }

        //
        // HexToByte
        //   Converts a hexadecimal string to a byte array. Used to convert encryption
        // key values from the configuration.
        //
        private byte[] HexToByte(string hexString)
        {
            byte[] returnBytes = new byte[hexString.Length / 2];
            for (int i = 0; i < returnBytes.Length; i++)
                returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
            return returnBytes;
        }

        //
        // MembershipProvider.FindUsersByName
        //
        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            IList<RegisterUserEntity> all = AccountAccess.GetInstance()
                .SearchEntities<RegisterUserEntity>("WHERE Name LIKE '%" + usernameToMatch + "%'");
            IList<RegisterUserEntity> list = PageFromEntities(all, pageIndex, pageSize, out totalRecords);
            MembershipUserCollection users = new MembershipUserCollection();
            foreach (RegisterUserEntity u in list)
            {
                users.Add(GetMembershipUser(u));
            }
            return users;
        }

        //
        // MembershipProvider.FindUsersByEmail
        //
        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            IList<RegisterUserEntity> all = AccountAccess.GetInstance()
                .SearchEntities<RegisterUserEntity>("WHERE Email LIKE '%" + emailToMatch + "%'");
            IList<RegisterUserEntity> list = PageFromEntities(all, pageIndex, pageSize, out totalRecords);
            MembershipUserCollection users = new MembershipUserCollection();
            foreach (RegisterUserEntity u in list)
            {
                users.Add(GetMembershipUser(u));
            }
            return users;
        }

        //
        // WriteToEventLog
        //   A helper function that writes exception detail to the event log. Exceptions
        // are written to the event log as a security measure to avoid private database
        // details from being returned to the browser. If a method does not return a status
        // or boolean indicating the action succeeded or failed, a generic exception is also 
        // thrown by the caller.
        //
        //private void WriteToEventLog(Exception e, string action)
        //{
        //    EventLog log = new EventLog();
        //    log.Source = eventSource;
        //    log.Log = eventLog;

        //    string message = "An exception occurred communicating with the data source.\n\n";
        //    message += "Action: " + action + "\n\n";
        //    message += "Exception: " + e.ToString();

        //    log.WriteEntry(message);
        //}

        private static IList<RegisterUserEntity> PageFromEntities(IList<RegisterUserEntity> entities, int pageIndex, int pageSize, out int totalRecords)
        {
            IList<RegisterUserEntity> list = new List<RegisterUserEntity>();
            totalRecords = entities.Count;
            for (int i = 0; i < totalRecords; i++)
            {
                if (i >= pageIndex * pageSize && i < (pageIndex + 1) * pageSize)
                    list.Add( entities[i] );
            }
            return list;
        }

        private RegisterUserEntity GetUserByName(string username)
        {
            return RegisterUserEntity.FetchByLoweredName(username.ToLower());
        }

        private RegisterUserEntity GetUserByEmail(string email)
        {
            return RegisterUserEntity.FetchByLoweredEmail(email.ToLower());
        }

    }
}